fix: Strip PAC bits from return addresses in POSIX crash handler#327
fix: Strip PAC bits from return addresses in POSIX crash handler#327awforsythe wants to merge 3 commits into
Conversation
refs: RUM-17370 ARM64 has a security feature called Pointer Authentication Codes (PAC), where the CPU cryptographically signs address values, encoding the signature in the upper bits of the 64-bit value. It can then verify these addresses before jumping to them, to detect stack manipulation etc. at the hardware level. The SDK's in-process crash handler for POSIX reads return addresses as it walks the stack frame. When running arm64 Linux builds in Docker, I noticed that these addresses weren't being correctly resolved to loaded modules for shared libraries. As it turns out, module resolution was failing because the upper bits were set by PAC, causing the resolved address to land outside the load range of the module. When compiled for ARMv8.3-a or later (__ARM_FEATURE_PAUTH), we use the xpaci instruction to strip PAC bits. This asks the hardware to remove exactly the signature bits using its own key and VA-width configuration, which is correct on kernels with 52-bit user VAs (CONFIG_ARM64_VA_BITS_52) where bits [51:48] are legitimate address bits that a hard-coded 48-bit mask would incorrectly zero. On ARMv8.0-8.2 hardware (no PAC, no 52-bit VA support), we fall back to masking the lower 48 bits.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f9747ab64d
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| asm("xpaci %0" : "+r"(raw_ret)); | ||
| #else | ||
| // ARMv8.0-8.2: no PAC, no 52-bit VA; mask to the 48-bit canonical address. | ||
| raw_ret &= 0x0000ffffffffffff; |
There was a problem hiding this comment.
Preserve 52-bit AArch64 return addresses
On AArch64 Linux builds that do not define __ARM_FEATURE_PAUTH (for example the default generic arm64 build here, since CMake does not set -march=armv8.3-a), this fallback now unconditionally clears bits [63:48]. On kernels configured for 52-bit user VAs, bits [51:48] can be legitimate address bits, so frames whose return addresses are above 48 bits get written with a different address than the /proc/self/maps module ranges and will not symbolize correctly; the non-PAUTH path should not truncate addresses unconditionally.
Useful? React with 👍 / 👎.
The previous approach guarded xpaci behind __ARM_FEATURE_PAUTH and fell back to a hard-coded 48-bit mask when the flag was absent. That flag is only defined when the compiler is invoked with -march=armv8.3-a or later, so on a generic arm64 build the mask would fire even on PAC-capable hardware — incorrectly zeroing bits [51:48] on kernels configured for 52-bit user VAs (CONFIG_ARM64_VA_BITS_52). xpaci is the right tool unconditionally: it asks the CPU to strip exactly the PAC bits using its own key and VA-width configuration, so it handles any VA width correctly. On pre-PAC hardware (ARMv8.0-8.2) it falls in the HINT space and executes as a NOP, which is also correct since those CPUs never encode PAC bits in addresses. Clang's integrated assembler accepts the instruction regardless of -march, so no compile-time feature guard is needed.
|
@codex review |
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4b158bf4e8
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| // assembler accepts the instruction regardless of -march, so no feature guard is | ||
| // needed. | ||
| uint64_t raw_ret = reinterpret_cast<uint64_t>(ret_addr); | ||
| asm("xpaci %0" : "+r"(raw_ret)); |
There was a problem hiding this comment.
Preserve generic Linux arm64 builds
In generic Linux AArch64 builds where the SDK is not compiled with -march=armv8.3-a, this inline asm does not assemble with Clang's integrated assembler; I checked with the repo's LLVM 20-era compiler via clang-20 -target aarch64-linux-gnu and it reports instruction requires: pauth. Since DD_CRASH_MODE=inprocess is the default and CMake does not add a PAC-enabled -march, Linux arm64 users can fail at compile time before the intended HINT/NOP behavior on older CPUs is relevant; encode the instruction with .inst or otherwise avoid requiring the assembler feature.
Useful? React with 👍 / 👎.
Clang's assembler rejects 'xpaci' unless the target architecture includes the pauth feature (i.e. -march=armv8.3-a or later). Without an explicit -march flag, aarch64-linux-gnu builds default to armv8-a and fail to compile the inline asm, even though the instruction is safe to run on all AArch64 hardware. Fix this by pinning the operand to x16 and emitting the instruction via .inst 0xDAC143F0 (the fixed encoding for xpaci x16). The assembler emits the bytes directly without needing to know about pauth. x16 (IP0) is a caller-saved scratch register that is safe to use in a signal handler. The CI Linux jobs all run on amd64 runners so the aarch64 code path is never compiled there; this would only be caught by a customer building natively or cross-compiling for arm64 Linux.
|
@codex review |
|
Codex Review: Didn't find any major issues. Hooray! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
refs: RUM-17370
ARM64 has a security feature called Pointer Authentication Codes (PAC), where the CPU cryptographically signs address values, encoding the signature in the upper bits of the 64-bit value. It can then verify these addresses before jumping to them, to detect stack manipulation etc. at the hardware level.
The SDK's in-process crash handler for POSIX reads return addresses as it walks the stack frame. When running arm64 Linux builds in Docker, I noticed that these addresses weren't being correctly resolved to loaded modules for shared libraries.
As it turns out, module resolution was failing because the upper bits were set by PAC, causing the resolved address to land outside the load range of the module.
The actual significant bits representing an address are typically encoded in the lower 48 bits, but they can also be 52 bits. This distinction is based on kernel and hardware configuration: it's not a static compile-time thing.
We use the
xpaciinstruction to mask out the PAC bits, ending up with the actual address value that we need for stack walking. The ARM instruction set guarantees thatxpaciis in theHINTspace, meaning that it's executed as aNOPon hardware that predates the addition of PAC. On such CPUs, there are no PAC bits to strip, so the value already represents a coherent address as-is.